/*
 * Copyright (c) 2013 Mohammad Mehdi Saboorian
 *
 * This is part of moor, a wrapper for libarchive
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include "ArchiveWriter.h"
#include "MemoryWriterCallback.h"
#include "ScopeExit.h"
#include "LogHelper.h"

#include "archive.h"
#include "archive_entry.h"

#include <stdlib.h>
#include <stdio.h>
#ifdef _WIN32
    #include <direct.h>
#else
    #include <unistd.h>
#endif
#include <stdexcept>
#include <fstream>
#include <iostream>

#include <sys/stat.h>


int32_t(*formatTable[])(archive*) = {
        archive_write_set_format_pax_restricted,
        archive_write_set_format_gnutar,
        archive_write_set_format_zip,
        archive_write_set_format_7zip
        };

int32_t(*compressionTable[])(archive*) = {
        archive_write_set_compression_none,
        archive_write_set_compression_gzip,
        archive_write_set_compression_bzip2,
        archive_write_set_compression_lzma
        };

int32_t archiveFileType[] = { AE_IFREG, AE_IFDIR };

void ArchiveWriter::checkError(const int32_t errCode, const bool closeBeforThrow)
{
    std::string error;
    if (errCode != ARCHIVE_OK && errCode != ARCHIVE_WARN)
        error = archive_error_string(m_archive);
    if (closeBeforThrow && errCode == ARCHIVE_FATAL)
        close();
    if (errCode != ARCHIVE_OK && errCode != ARCHIVE_WARN)
        throw std::runtime_error(error);
}


ArchiveWriter::ArchiveWriter(const std::string& archiveFileName
    , const eFormats& format, const eCompressions& compression)
    : m_open(true), m_archive(archive_write_new()), m_entry(archive_entry_new())
    , m_archiveFileName(archiveFileName)
    , m_format(format), m_compression(compression)
	, m_code(0)
{
    //set archive format
    checkError((formatTable[m_format](m_archive)), true);
    //set archive compression
    checkError((compressionTable[m_compression](m_archive)), true);
    checkError(archive_write_open_filename(m_archive, m_archiveFileName.c_str()), true);
}

ArchiveWriter::ArchiveWriter(std::list<unsigned char>& outBuffer
    , const eFormats& format, const eCompressions& compression)
    : m_open(true), m_archive(archive_write_new()), m_entry(archive_entry_new())
    , m_archiveFileName(""), m_format(format)
    , m_compression(compression)
	, m_code(0)
{
    //set archive format
    checkError((formatTable[m_format](m_archive)), true);
    //set archive compression
    checkError((compressionTable[m_compression](m_archive)), true);
    checkError(write_open_memory(m_archive, &outBuffer), true);
}

ArchiveWriter::ArchiveWriter(unsigned char* outBuffer, size_t* size
    , const eFormats& _format, const eCompressions& _compression)
    : m_open(true), m_archive(archive_write_new()), m_entry(archive_entry_new())
    , m_archiveFileName(""), m_format(_format)
    , m_compression(_compression)
	, m_code(0)
{
    //set archive format
    checkError((formatTable[m_format](m_archive)), true);
    //set archive compression
    checkError((compressionTable[m_compression](m_archive)), true);
    checkError(archive_write_open_memory(m_archive, outBuffer, *size, size), true);
}

ArchiveWriter::ArchiveWriter(const eFormats& format, const eCompressions& compression)
    : m_archive(archive_write_new()), m_entry(archive_entry_new())
    , m_archiveFileName(""), m_format(format), m_compression(compression)
	, m_code(0)
{
    //set archive format
    checkError((formatTable[m_format](m_archive)), true);
    //set archive compression
    checkError((compressionTable[m_compression](m_archive)), true);
}

bool ArchiveWriter::open(const std::string& archiveFileName)
{
    try
    {
        checkError(archive_write_open_filename(m_archive, archiveFileName.c_str()), false);
        m_open = true;
        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }

    return false;
}

bool ArchiveWriter::open(std::list<unsigned char>& outBuffer)
{
    try
    {
        checkError(write_open_memory(m_archive, &outBuffer), false);
        m_open = true;
        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }

    return false;
}

bool ArchiveWriter::open(unsigned char * outBuffer, size_t* size)
{
    try
    {
        checkError(archive_write_open_memory(m_archive, outBuffer, *size, size), false);
        m_open = true;
        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }

    return false;
}

ArchiveWriter::~ArchiveWriter()
{
    try
    {
        close();
    }
    catch (...)
    {
    }
}

bool ArchiveWriter::setFormatOption(const std::string& option, const std::string& value)
{
    try
    {
        checkError(archive_write_set_format_option(m_archive, NULL, option.c_str(), value.c_str()), false);
        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }

    return false;
}

void ArchiveWriter::addHeader(const std::string& entryName, const eFileTypes entryType, const long long size, const int32_t permission)
{
    m_entry = archive_entry_clear(m_entry);
    archive_entry_set_pathname(m_entry, entryName.c_str());
    archive_entry_set_perm(m_entry, permission);
    archive_entry_set_filetype(m_entry, archiveFileType[entryType]);
    archive_entry_set_size(m_entry, size);
    checkError(archive_write_header(m_archive, m_entry));
}

void ArchiveWriter::addHeader(const std::string& filePath, const std::string& entryName)
{
    struct archive* a = archive_read_disk_new();
    ScopeExit se([&a]
    {
        if (a != NULL)
        {
            archive_read_close(a);
            archive_read_free(a);
        }
    });

    m_entry = archive_entry_clear(m_entry);
    archive_entry_set_pathname(m_entry, entryName.c_str());
    archive_entry_copy_sourcepath(m_entry, filePath.c_str());
    checkError(archive_read_disk_entry_from_file(a, m_entry, -1, 0));
    checkError(archive_write_header(m_archive, m_entry));
}

void ArchiveWriter::addContent(const char byte)
{
    archive_write_data(m_archive, &byte, 1);
}

void ArchiveWriter::addContent(const char* bytes, uint64_t size)
{
    archive_write_data(m_archive, bytes, static_cast<size_t>(size));
}

void ArchiveWriter::addFinish()
{
    checkError(archive_write_finish_entry(m_archive), false);
}

bool ArchiveWriter::addFile(const std::string& filePath, const std::string& entryName)
{
    try
    {
        struct stat fileStat;
        if (stat(filePath.c_str(), &fileStat) < 0)
        {
            switch (errno)
            {
                case ENOENT:
                {
                    m_code = (int32_t)eFileNotFound;
                    m_err = "file not found.";
					return false;
                }
                default:
                {
                    m_code = (int32_t)eFileNotValid;
                    m_err = "file is not valid.";
					return false;
                }
            }
        }
        addHeader(filePath, entryName.empty() ? filePath : entryName);

        if (fileStat.st_mode & S_IFREG)
        {
            std::fstream entryFile(filePath.c_str(), std::ios::in | std::ios::binary);
            char buff[8192];
            while (entryFile.good())
            {
                entryFile.read(buff, 8192);
                archive_write_data(m_archive, buff, static_cast<size_t>(entryFile.gcount()));
            }
            entryFile.close();
        }
        else
        {
            m_code = (int32_t)eEntryFileTypeNotSupported;
            m_err = "Entry file type not yet supported.";
			return false;
        }

        addFinish();

        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }

    return false;
}

bool ArchiveWriter::addFile(const std::string& entryName, const unsigned char * data, uint64_t size)
{
    try
    {
        addHeader(entryName, eFileType_Regular, size);
        addContent((char*)data, size);
        addFinish();

        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }

    return false;
}

bool ArchiveWriter::addDirectory(const std::string& directoryName)
{
    try
    {
        addHeader(directoryName, eFileType_Directory, 0777);
        addFinish();

        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }

    return false;

}

bool ArchiveWriter::close()
{
    try
    {
        if (m_open)
        {
            if (m_archive != NULL)
            {
                checkError(archive_write_close(m_archive));
                checkError(archive_write_free(m_archive));
            }
            if (m_entry != NULL)
                archive_entry_free(m_entry);

            m_open = false;
        }
        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }

    return false;
}


bool ArchiveWriter::compress(const std::string& basePath, std::vector<std::string>& fileList)
{
    try
    {
		std::string currentDir;
        char buffer[1024] = { 0 };
        if (basePath.length() != 0)
        {
            getcwd(buffer, 1024);
			currentDir = buffer;

            int32_t ret = -1;
            ret= chdir(basePath.c_str());
            if (ret != 0)
            {
                m_code = eDirectoryPathError;
                m_err = "Base Directory Path Error";
                return false;
            }
        }
        std::vector<std::string>::iterator it = fileList.begin();
        for (; it != fileList.end(); it++)
        {
            std::size_t pos = basePath.length();
            std::string sub = (*it).substr(pos);
            if (sub.size() > 0)
            {
                sub.erase(0, sub.find_first_not_of("\\"));
                sub.erase(0, sub.find_first_not_of("/"));
                sub.erase(sub.find_last_not_of("/") + 1);
                sub.erase(sub.find_last_not_of("\\") + 1);
            }

            addFile(sub.c_str());
        }
        close();

		// back to origin directory
		if (basePath.length() != 0)
		{
			int32_t ret = -1;
			ret = chdir(currentDir.c_str());
		}

        return true;
    }
    catch (std::runtime_error& e)
    {
		ERR << "error: " << e.what();
        return false;
    }
    return false;
}


int32_t ArchiveWriter::getLastErrorCode() const
{
    return m_code;
}

std::string ArchiveWriter::getLastError() const
{
    return m_err;
}
